
from view.endSrc.MySqlConn import MySqlConn
from view.endSrc.DBConfig import DBConfig
from view.endSrc.MyTools import MyTools

import numpy as np
from numpy import genfromtxt


class tObserver:

    def setSqlConn(self, dbConn: MySqlConn):
        '''
            let use set sql connector dynamatically
        :param dbConn:
        :return:
        '''
        if dbConn is not None:
            self._dbconn = dbConn
            self._logger = dbConn.logger

    def __init__(self, dbconn=None):
        self._dbconn = None
        self._logger = None
        self.setSqlConn(dbconn)

        self.id = None
        self.sessionId = None       # related to which record of the table tIniceRuns
        self.distancesFileName = None
        self.clusterEstimatorClassName = None
        self.viewPoint = None           # JSON dict
        self.nCenters = None
        self.centers = None             # JSON dict
        self.One2OneDistClassName = None
        self.N2N2DistClassName = None

        self.filtersCfg = None          # JSON dict
        self.nComponentsGuess = None    # int
        self.mixedParamsCfg = None     # JSON dict
        self.bestClusterEstimatorId = None  # id of the best cluster estimator record in 'clusterEstimatorClassName' table

        ### memory from files or JSON
        self.m_distances = None         # self.distancesFileName, list<float>
        self.m_centers = None           # list<np.ndarray_1D>
        self.m_bestCluEstTable = None   # such as tGammaClustersEstimatorBy1D


    def __str__(self):
        '''
            for simple test
        :return:
        '''
        tmp = '---------- tObserver----------------\n'
        tmp += 'id: ' + str(self.id) + '\n'
        tmp += 'sessionId: ' + str(self.sessionId) + '\n'
        tmp += 'distancesFileName: ' + str(self.distancesFileName) + '\n'
        tmp += 'clusterEstimatorClassName: ' + str(self.clusterEstimatorClassName) + '\n'
        tmp += 'viewPoint: ' + str(self.viewPoint) + '\n'
        tmp += 'nCenters: ' + str(self.nCenters) + '\n'
        tmp += 'centers: ' + str(self.centers) + '\n'

        tmp += 'One2OneDistClassName: ' + str(self.One2OneDistClassName) + '\n'
        tmp += 'N2N2DistClassName: ' + str(self.N2N2DistClassName) + '\n'

        tmp += 'nComponentsGuess' + str(self.nComponentsGuess) + '\n'
        tmp += 'filtersCfg: ' + str(self.filtersCfg) + '\n'
        tmp += 'mixedParamsCfg: ' + str(self.mixedParamsCfg) + '\n'
        tmp += 'bestClusterEstimatorId: ' + str(self.bestClusterEstimatorId) + '\n'

        tmp += 'm_distances: ' + str(self.m_distances) + '\n'
        tmp += 'm_centers: ' + str(self.m_centers) + '\n'

        return tmp


    def createTable(self):

        assert self._dbconn is not None, 'please set setSqlConn first'

        if self._dbconn.tableExists('tObserver'):
            print('exist..tObserver, if you want to drop it, please call MySqlConn.dropTable')
            return True

        sql = """create table tObserver(
                id int primary key auto_increment not null,
                sessionId int not null,
                distancesFileName CHAR(200),
                clusterEstimatorClassName CHAR(200) not null,
                viewPoint JSON not null,
                nCenters int,
                centers JSON,
                One2OneDistClassName CHAR(200) not null,
                N2N2DistClassName CHAR(200) not null,
                filtersCfg JSON not null,
                nComponentsGuess int,
                mixedParamsCfg JSON,
                bestClusterEstimatorId int) """

        # foreign key(sessionId) references tIniceRuns(id)), do not use it for development

        if not self._dbconn.createTable(sql):
            self._logger.write('tObserver.createTable is failed')
            return False
        return True


    def createRow(self,
                  sessionId, clusterEstimatorClassName,
                  viewPoint: np.ndarray,
                  One2OneDistClassName, N2N2DistClassName,
                  filterCfgJSONDict, mixedParamsCfgJSONDict=None):
        '''
            return new db id if succeed
        :param sessionId:
        :param clusterEstimatorClassName:
        :param viewPoint:
        :param One2OneDistClassName:
        :param N2N2DistClassName:
        :return: new db id
        '''

        assert self._dbconn is not None, 'please set setSqlConn first'

        assert isinstance(sessionId, int) and sessionId > 0
        assert isinstance(viewPoint, np.ndarray)
        assert isinstance(clusterEstimatorClassName, str) and \
            isinstance(One2OneDistClassName, str) and \
            isinstance(N2N2DistClassName, str)

        MyTools.checkMemberShape(viewPoint, (), 'tObserver.createRow.viewPoint')  # members of viewPoint must be scalars
        assert MyTools.isJsonDumpable(filterCfgJSONDict)
        assert mixedParamsCfgJSONDict is None or MyTools.isJsonDumpable(mixedParamsCfgJSONDict)

        vpJsonDict = {'vp': viewPoint.tolist()}

        #                 id int primary key auto_increment not null,
        #                 sessionId int not null,
        #                 distancesFileName CHAR(200),
        #                 clusterEstimatorClassName CHAR(200) not null,
        #                 viewPoint JSON not null,
        #                 nCenters int,
        #                 centers JSON,
        #                 One2OneDistClassName CHAR(200) not null,
        #                 N2N2DistClassName CHAR(200) not null,
        #                 filtersCfg JSON not null,
        #                 nComponentsGuess int,
        #                 mixedParamsCfg JSON,
        #                 bestClusterEstimatorId int)

        rtup = [sessionId,
                clusterEstimatorClassName,
                MyTools.getJsonByDict(vpJsonDict),
                One2OneDistClassName,
                N2N2DistClassName,
                MyTools.getJsonByDict(filterCfgJSONDict)]
        # build a sql statement
        sql = "INSERT INTO tObserver( " \
                "sessionId, clusterEstimatorClassName, " \
                "viewPoint, " \
                "One2OneDistClassName, N2N2DistClassName," \
                " filtersCfg)" \
                "VALUES ( %d, '%s', " \
                        " '%s', " \
                        " '%s', '%s', '%s' ) " % tuple(rtup)
        if mixedParamsCfgJSONDict is not None:
            rtup.append(MyTools.getJsonByDict(mixedParamsCfgJSONDict))
            sql = "INSERT INTO tObserver( " \
                  "sessionId, clusterEstimatorClassName, " \
                  "viewPoint, " \
                  "One2OneDistClassName, N2N2DistClassName," \
                  " filtersCfg, mixedParamsCfg)" \
                  "VALUES ( %d, '%s', " \
                  " '%s', " \
                  " '%s', '%s', '%s', '%s' ) " % tuple(rtup)

        newId = self._dbconn.insertRetId(sql)
        if newId is not None:
            self.id = newId
            self.sessionId = sessionId
            self.clusterEstimatorClassName = clusterEstimatorClassName
            self.viewPoint = viewPoint
            self.One2OneDistClassName = One2OneDistClassName
            self.N2N2DistClassName = N2N2DistClassName

            self.filtersCfg = filterCfgJSONDict
            self.mixedParamsCfg = mixedParamsCfgJSONDict        # can be None

            self.distancesFileName = None
            self.nCenters = None
            self.centers = None

            self.m_centers = None
            self.m_distances = None
        else:
            self._logger.write('tObserver.createRow: failed to insert..' + str(rtup))
        return newId


    def readRow(self, id):

        assert self._dbconn is not None, 'please set setSqlConn first'
        assert isinstance(id, int) and id > 0

        #                 id int primary key auto_increment not null,
        #                 sessionId int not null,
        #                 distancesFileName CHAR(200),
        #                 clusterEstimatorClassName CHAR(200) not null,
        #                 viewPoint JSON not null,
        #                 nCenters int,
        #                 centers JSON,
        #                 One2OneDistClassName CHAR(200) not null,
        #                 N2N2DistClassName CHAR(200) not null,
        #                 filtersCfg JSON not null,
        #                 nComponentsGuess int,
        #                 mixedParamsCfg JSON,
        #                 bestClusterEstimatorId int)

        sql = "SELECT * FROM tObserver WHERE id = %d" % id
        rows = self._dbconn.read(sql)
        if rows is None or len(rows) == 0:
            self._logger.write('tObserver.readRow..failed to read by id= ' + str(id))
            return False
        row = rows[0]

        self.id, self.sessionId, self.distancesFileName, \
        self.clusterEstimatorClassName, self.viewPoint,\
        self.nCenters, self.centers, self.One2OneDistClassName,\
        self.N2N2DistClassName, self.filtersCfg, \
        self.nComponentsGuess, self.mixedParamsCfg,\
        self.bestClusterEstimatorId = row

        self.m_distances = None
        if self.distancesFileName is not None:
            pathPrefix = DBConfig().dataFilesRelPathPrefix
            try:

                self.m_distances = genfromtxt(pathPrefix + self.distancesFileName,
                                      delimiter=',')  # use np.csv for high performance, instead of pd
                self.m_distances = self.m_distances.tolist()
            except Exception as e:
                self._logger.write(
                    'tObserver.readRow..failed to read distances vector from the file: ' + self.distancesFileName)
                return False

        # viewPoint/filterCfg in DB is NOT NULL
        vpJsonDict = MyTools.getDicByJson(self.viewPoint)
        self.viewPoint = np.asarray(vpJsonDict['vp'])
        self.filtersCfg = MyTools.getDicByJson(self.filtersCfg)
        self.mixedParamsCfg = MyTools.getDicByJson(self.mixedParamsCfg)   #getDicByJson can process when param is None

        # load centers
        self.m_centers = None       # m_centers is a list< np.ndarray_vec >
        if self.centers is None:    # self.centers is a JSON string
            return True

        ctJsonDict = MyTools.getDicByJson(self.centers)
        cts = ctJsonDict['cts']
        ctsList = []
        for c in cts:
            # each member of a center vector is a scalar
            MyTools.checkMemberShape(c, (), 'tObserver.readRow.centers')
            ctsList.append(np.asarray(c))

        self.m_centers = ctsList                # list<vec>

        # load best cluster estimator table for WebUI
        tableName = 't' + self.clusterEstimatorClassName
        clsPath = tableName + '.' + tableName
        if self.bestClusterEstimatorId is not None:
            try:

                cls = MyTools.getClass(clsPath)
                table = cls()
                table.readRow(self.bestClusterEstimatorId)

                self.m_bestCluEstTable = table
            except Exception as e:
                self._logger.write('tObserver.readRow..omit.load table: ' + tableName + ' failed')
                # since not all users need both tObserver and tGammaClustersEstimatorBy1D at the same time.

        return True

    def _removeDistancesFile(self):

        rtup = (self.id)
        sql = "SELECT distancesFileName FROM tObserver WHERE id=%d" % rtup
        rows = self._dbconn.read(sql)

        # print('rows: ', rows)  # ((None,),)

        if rows is None:
            tmp = 'tObserver._removeDistancesFile..'
            tmp += 'select distancesFileName..is failed for id=' + str(self.id)
            self._logger.write(tmp)
            return False

        if len(rows) == 0:
            return True

        self.distancesFileName = rows[0][0]
        if self.distancesFileName is None:
            return True   # ok, it does not exist

        # try to remove it
        pathPrefix = DBConfig().dataFilesRelPathPrefix
        return MyTools.removeFile(pathPrefix + self.distancesFileName)

    def updateDistances(self, distances: list, nComponentsGuess: int):

        assert self._dbconn is not None, 'please set setSqlConn first'
        assert self.id > 0, 'must have a valid DB id'

        MyTools.checkMemberShape(distances, (), 'tObserver.updateDistances.distances')

        #                 id int primary key auto_increment not null,
        #                 sessionId int not null,
        #                 distancesFileName CHAR(200),
        #                 clusterEstimatorClassName CHAR(200) not null,
        #                 viewPoint JSON not null,
        #                 nCenters int,
        #                 centers JSON,
        #                 One2OneDistClassName CHAR(200) not null,
        #                 N2N2DistClassName CHAR(200) not null,
        #                 filtersCfg JSON not null,
        #                 nComponentsGuess int,
        #                 mixedParamsCfg JSON,
        #                 bestClusterEstimatorId int)


        # remove distances if exist
        self._removeDistancesFile()

        distanceVec = np.asarray(distances)  # to numpy.vec

        # write distances to file
        pathPrefix = DBConfig().dataFilesRelPathPrefix
        date = MyTools.getDatetimeFileName()
        fName= 'tObserver_distan_' + 'id' + str(self.id) + 'n' + str(len(distanceVec)) + '_' + date + '.csv'
        try:
            np.savetxt(pathPrefix + fName, distanceVec, delimiter=",")
        except Exception as e:
            self._logger.write('tObserver.updateDistances..failed to write to files: ' + str(e) + \
                               ' for id = ' + str(self.id))
            return False

        # write to DB
        rtup = (fName,
                nComponentsGuess,
                self.id)
        # create sql
        sql = "UPDATE tObserver SET " \
              " distancesFileName='%s', nComponentsGuess=%d" \
              " WHERE id=%d " % rtup

        if not self._dbconn.insDelUpd(sql):
            self._logger.write('tObserver.updateDistances DB updating is failed for id= ' + str(self.id))
            return False

        # set memory fields at last
        self.m_distances = distances
        self.nComponentsGuess = nComponentsGuess
        return True

    def updateCenters(self, centers: list, bestClusterEstimatorId: int):

        assert self._dbconn is not None, 'please set setSqlConn first'
        assert self.id > 0, 'must have a valid DB id'

        assert isinstance(centers, list)
        assert isinstance(bestClusterEstimatorId, int)

        shp = np.shape(centers[0])
        MyTools.checkMemberShape(centers, shp, 'tObserver.updateCenters.centers')

        #                 id int primary key auto_increment not null,
        #                 sessionId int not null,
        #                 distancesFileName CHAR(200),
        #                 clusterEstimatorClassName CHAR(200) not null,
        #                 viewPoint JSON not null,
        #                 nCenters int,
        #                 centers JSON,
        #                 One2OneDistClassName CHAR(200) not null,
        #                 N2N2DistClassName CHAR(200) not null,
        #                 filtersCfg JSON not null,
        #                 nComponentsGuess int,
        #                 mixedParamsCfg JSON,
        #                 bestClusterEstimatorId int)

        centerList = centers
        if shp != ():
            centerList = []
            for c in centers:
                centerList.append(list(c))

        ctsDict = {'cts': centerList}
        ctsJSON = MyTools.getJsonByDict(ctsDict)

        # write to DB
        rtup = (len(centerList),
                ctsJSON,
                bestClusterEstimatorId,
                self.id)
        # create sql
        sql = "UPDATE tObserver SET " \
              "nCenters = %d, centers = '%s'," \
              " bestClusterEstimatorId = %d " \
              "WHERE id= %d " % rtup

        if not self._dbconn.insDelUpd(sql):
            self._logger.write('tObserver.updateCenters DB updating is failed for id= ' + self.id)
            return False

        # set memory fields at last
        self.centers = centers
        self.nCenters = len(centers)
        return True

    def getObConfigDetail(self, sessionId):

        assert self._dbconn is not None, 'please set setSqlConn first'
        assert isinstance(sessionId, int) and sessionId > 0

        sql = "SELECT One2OneDistClassName, N2N2DistClassName, clusterEstimatorClassName " \
              "from tObserver WHERE sessionId='%d'" % sessionId
        rows = self._dbconn.read(sql)
        if rows is None:
            self._logger.write('tObserver.getObConfigDetail..failed to read by sessionId= ' + str(sessionId))
            return None

        if  len(rows) == 0:
            self._logger.write('tObserver.getObConfigDetail.. read nothing by sessionId= ' + str(sessionId))

        return rows


    def getObserverDetailBySession(self, sessionId):

        assert self._dbconn is not None, 'please set setSqlConn first'
        assert isinstance(sessionId, int) and sessionId > 0

        sql = "SELECT * from tObserver WHERE sessionId='%d'" % sessionId
        rows = self._dbconn.read(sql)
        if rows is None:
            self._logger.write('tObserver.getObserverDetailBySession..failed to read by sessionId= ' + str(sessionId))
            return None

        if  len(rows) == 0:
            self._logger.write('tObserver.getObserverDetailBySession.. read nothing by sessionId= ' + str(sessionId))

        return rows